/*
 * JSwiff is an open source Java API for Macromedia Flash file generation
 * and manipulation
 *
 * Copyright (C) 2004-2008 Ralf Terdic (contact@jswiff.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.jswiff.swfrecords.tags;

import com.jswiff.io.InputBitStream;
import com.jswiff.io.OutputBitStream;
import com.jswiff.swfrecords.CXformWithAlpha;
import com.jswiff.swfrecords.ClipActions;
import com.jswiff.swfrecords.Matrix;
import com.jswiff.swfrecords.tags.interfaces.IPlaceObject;

import java.io.IOException;

/**
 * <p>
 * With this tag, an instance of a character can be added to the display list.
 * Besides, unlike <code>PlaceObject</code>, it can modify the attributes of a
 * character that is already on the display list.
 * </p>
 * 
 * <p>
 * The only mandatory attribute is the character depth. A character that is
 * already on the display list can be identified by its depth alone, as there
 * can be only one character at a given depth. If a new character is added to
 * the display list, the character ID is needed.
 * </p>
 * 
 * <p>
 * If the <code>move</code> flag is set, the character at the given depth is
 * removed. If no character ID is set, the removed character is redisplayed at
 * the same depth with new attributes. If the character ID is set, the
 * corresponding character replaces the removed one.
 * </p>
 * 
 * <p>
 * A transform matrix and a color tranform can be specified in order to
 * determine the position, rotation, scale and color of the character to be
 * displayed.
 * </p>
 * 
 * <p>
 * The morph ratio applies to characters defined with
 * <code>DefineMorphShape</code> and specifies how far the morph has progressed.
 * </p>
 * 
 * <p>
 * A (non-zero) clip depth indicates that the character is a clipping character
 * which masks depths up to and including the specified value (e.g. if a
 * character was placed at depth 1 with a clip depth of 4, all depths above 1,
 * up to and including depth 4, will be masked by the shape placed at depth 1;
 * characters placed at depths above 4 will not be masked).
 * </p>
 * 
 * <p>
 * The character instance can be given a name which it can later be referenced
 * by (e.g. within <code>With</code>).
 * </p>
 * 
 * <p>
 * Finally, if the character to be placed is a sprite, one or more event
 * handlers (clip actions) can be defined.
 * </p>
 * 
 * @see PlaceObject
 * @see DefineMorphShape
 * @since SWF 3
 */
public final class PlaceObject2 extends Tag implements IPlaceObject
{
	private boolean move;
	private int depth;
	private int characterId;
	private Matrix matrix;
	private CXformWithAlpha colorTransform;
	private int ratio;
	private String name;
	private int clipDepth;
	private ClipActions clipActions;
	private boolean hasClipActions;
	private boolean hasClipDepth;
	private boolean hasName;
	private boolean hasRatio;
	private boolean hasColorTransform;
	private boolean hasMatrix;
	private boolean hasCharacter;

	/**
	 * Creates a new PlaceObject2 tag.
	 * 
	 * @param depth
	 *            depth the character is placed at
	 */
	public PlaceObject2(int depth)
	{
		code = TagConstants.PLACE_OBJECT_2;
		this.depth = depth;
	}

	PlaceObject2()
	{
		// empty
	}

	/**
	 * Sets the character ID. If this ID is set, the corresponding character is
	 * displayed at the depth specified with the constructor.
	 * 
	 * @param characterId
	 *            The characterId to set.
	 */
	public void setCharacterId(int characterId)
	{
		this.characterId = characterId;
		hasCharacter = true;
	}

	/**
	 * Returns the character ID. If this ID is set, the corresponding character
	 * is displayed at the depth specified with the constructor. Check with
	 * <code>hasCharacter()</code> if set.
	 * 
	 * @return Returns the characterId.
	 */
	public int getCharacterId()
	{
		return characterId;
	}

	/**
	 * Sets the event handlers (only for sprite characters).
	 * 
	 * @param clipActions
	 *            event handlers
	 */
	public void setClipActions(ClipActions clipActions)
	{
		this.clipActions = clipActions;
		hasClipActions = (clipActions != null);
	}

	/**
	 * Returns the event handlers (only for sprite characters). Check with
	 * <code>hasClipActions()</code> if set.
	 * 
	 * @return Returns the clipActions.
	 */
	public ClipActions getClipActions()
	{
		return clipActions;
	}

	/**
	 * Sets the clip depth, indicating that the character is a clipping
	 * character which masks characters at depths up to and including the
	 * specified clip depth
	 * 
	 * @param clipDepth
	 *            the clip depth
	 */
	public void setClipDepth(int clipDepth)
	{
		this.clipDepth = clipDepth;
		hasClipDepth = true;
	}

	/**
	 * Returs the clip depth (which indicates that the character is a clipping
	 * character masking characters at depths up to and including the specified
	 * clip depth). Check with <code>hasclipDepth()</code> if set.
	 * 
	 * @return clip depth of character
	 */
	public int getClipDepth()
	{
		return clipDepth;
	}

	/**
	 * Sets the color transform, allowing color effects to be applied to the
	 * character to be displayed.
	 * 
	 * @param colorTransform
	 *            a color transform
	 */
	public void setColorTransform(CXformWithAlpha colorTransform)
	{
		this.colorTransform = colorTransform;
		hasColorTransform = (colorTransform != null);
	}

	/**
	 * Returns the color transform which allows color effects to be applied to
	 * the character to be displayed. Check with
	 * <code>hasColorTransform()</code> if set.
	 * 
	 * @return Returns the colorTransform.
	 */
	public CXformWithAlpha getColorTransform()
	{
		return colorTransform;
	}

	/**
	 * Sets the depth the character will be displayed at.
	 * 
	 * @param depth
	 *            display depth
	 */
	public void setDepth(int depth)
	{
		this.depth = depth;
	}

	/**
	 * Returns the depth the character will be displayed at.
	 * 
	 * @return display depth
	 */
	public int getDepth()
	{
		return depth;
	}

	/**
	 * Sets the tranform matrix, specifying position, scale, rotation etc. of
	 * the character to be displayed.
	 * 
	 * @param matrix
	 *            transform matrix
	 */
	public void setMatrix(Matrix matrix)
	{
		this.matrix = matrix;
		hasMatrix = (matrix != null);
	}

	/**
	 * Returns the tranform matrix, which specifies position, scale, rotation
	 * etc. of the character to be displayed. Check with
	 * <code>hasMatrix()</code> if set.
	 * 
	 * @return transform matrix
	 */
	public Matrix getMatrix()
	{
		return matrix;
	}

	/**
	 * <p>
	 * Sets the move flag. If set, the character at the given depth is removed.
	 * It is replaced either by a modified instance of the removed character or
	 * (if a character ID is specified) by a new character.
	 * </p>
	 * 
	 * <p>
	 * Mainly used for moving a character: first frame specifies depth,
	 * character ID and initial matrix. Subsequent frames have the move flag set
	 * for this depth and replace the translate values of the matrix with new
	 * ones.
	 * </p>
	 */
	public void setMove()
	{
		move = true;
	}

	/**
	 * Sets or clears the move flag.
	 * 
	 * @param move
	 *            value of move flag
	 * 
	 * @see PlaceObject2#setMove()
	 */
	public void setMove(boolean move)
	{
		this.move = move;
	}

	/**
	 * Checks the move flag. If set, the character at the given depth is
	 * removed. It is replaced either by a modified instance of the removed
	 * character or (if a character ID is specified) by a new character.
	 * 
	 * @return <code>true</code> if move flag set, else <code>false</code>
	 */
	public boolean isMove()
	{
		return move;
	}

	/**
	 * Assigns a name to the instance of the character to be placed, in order to
	 * be able to reference this instance by the assigend name (e.g. within
	 * <code>With</code>).
	 * 
	 * @param name
	 *            instance name
	 */
	public void setName(String name)
	{
		this.name = name;
		hasName = (name != null);
	}

	/**
	 * Returns the name assigned to the instance of the character to be placed,
	 * in order to be able to reference this instance by the assigend name (e.g.
	 * within <code>With</code>). Check with <code>hasName()</code> if set.
	 * 
	 * @return instance name
	 */
	public String getName()
	{
		return name;
	}

	/**
	 * Sets the morph ratio which indicates how far the morph has progressed
	 * (only for characters defined with <code>DefineMorphShape</code>). Values
	 * between 0 and 65535 are permitted. A ratio of 0 displays the character at
	 * morph start, 65535 causes the character to be displayed at the end of the
	 * morph. Values between 0 and 65535 cause the Flash Player to interpolate
	 * between start and end shapes.
	 * 
	 * @param ratio
	 *            morph ratio
	 */
	public void setRatio(int ratio)
	{
		if (ratio < 0)
		{
			this.ratio = 0;
		}
		else if (ratio > 65535)
		{
			this.ratio = 65535;
		}
		else
		{
			this.ratio = ratio;
		}
		hasRatio = true;
	}

	/**
	 * <p>
	 * Sets the morph ratio which indicates how far the morph has progressed
	 * (only for characters defined with <code>DefineMorphShape</code>). Values
	 * between 0 and 65535 are returned. A ratio of 0 displays the character at
	 * morph start, 65535 causes the character to be displayed at the end of the
	 * morph. Values between 0 and 65535 cause the Flash Player to interpolate
	 * between start and end shapes.
	 * </p>
	 * 
	 * <p>
	 * Check with <code>hasRatio()</code> if set.
	 * </p>
	 * 
	 * @return Returns the ratio.
	 */
	public int getRatio()
	{
		return ratio;
	}

	/**
	 * Checks whether the character ID is set.
	 * 
	 * @return <code>true</code> if character ID set, else <code>false</code>
	 */
	public boolean hasCharacter()
	{
		return hasCharacter;
	}

	/**
	 * Checks whether clip actions (sprite event handlers) have been specified.
	 * 
	 * @return <code>true</code> if clip actions set, else <code>false</code>
	 */
	public boolean hasClipActions()
	{
		return hasClipActions;
	}

	/**
	 * Checks if the value of the clip depth was set.
	 * 
	 * @return <code>true</code> if clip depth set, else <code>false</code>
	 */
	public boolean hasClipDepth()
	{
		return hasClipDepth;
	}

	/**
	 * Checks whether the color transform is specified.
	 * 
	 * @return <code>true</code> if color transform set, else <code>false</code>
	 */
	public boolean hasColorTransform()
	{
		return hasColorTransform;
	}

	/**
	 * Checks if a transform matrix is specified.
	 * 
	 * @return <code>true</code> if transform matrix set, else
	 *         <code>false</code>
	 */
	public boolean hasMatrix()
	{
		return hasMatrix;
	}

	/**
	 * Checks if a name is assigned to the character instance to be displayed.
	 * 
	 * @return <code>true</code> if character instance name set, else
	 *         <code>false</code>
	 */
	public boolean hasName()
	{
		return hasName;
	}

	/**
	 * Checks if the morph ratio is set.
	 * 
	 * @return <code>true</code> if ratio set, else <code>false</code>
	 */
	public boolean hasRatio()
	{
		return hasRatio;
	}

	protected void writeData(OutputBitStream outStream) throws IOException
	{
		outStream.writeBooleanBit(hasClipActions);
		outStream.writeBooleanBit(hasClipDepth);
		outStream.writeBooleanBit(hasName);
		outStream.writeBooleanBit(hasRatio);
		outStream.writeBooleanBit(hasColorTransform);
		outStream.writeBooleanBit(hasMatrix);
		outStream.writeBooleanBit(hasCharacter);
		outStream.writeBooleanBit(move);
		outStream.writeUI16(depth);
		if (hasCharacter)
		{
			outStream.writeUI16(characterId);
		}
		if (hasMatrix)
		{
			matrix.write(outStream);
		}
		if (hasColorTransform)
		{
			colorTransform.write(outStream);
		}
		if (hasRatio)
		{
			outStream.writeUI16(ratio);
		}
		if (hasName)
		{
			outStream.writeString(name);
		}
		if (hasClipDepth)
		{
			outStream.writeUI16(clipDepth);
		}
		if (hasClipActions)
		{
			clipActions.write(outStream, getSWFVersion());
		}
	}

	void setData(byte[] data) throws IOException
	{
		InputBitStream inStream = new InputBitStream(data);
		if (getSWFVersion() < 6)
		{
			if (isJapanese())
			{
				inStream.setShiftJIS(true);
			}
			else
			{
				inStream.setANSI(true);
			}
		}
		hasClipActions = inStream.readBooleanBit();
		hasClipDepth = inStream.readBooleanBit();
		hasName = inStream.readBooleanBit();
		hasRatio = inStream.readBooleanBit();
		hasColorTransform = inStream.readBooleanBit();
		hasMatrix = inStream.readBooleanBit();
		hasCharacter = inStream.readBooleanBit();
		move = inStream.readBooleanBit();
		depth = inStream.readUI16();
		if (hasCharacter)
		{
			characterId = inStream.readUI16();
		}
		if (hasMatrix)
		{
			matrix = new Matrix(inStream);
		}
		if (hasColorTransform)
		{
			colorTransform = new CXformWithAlpha(inStream);
		}
		if (hasRatio)
		{
			ratio = inStream.readUI16();
		}
		if (hasName)
		{
			name = inStream.readString();
		}
		if (hasClipDepth)
		{
			clipDepth = inStream.readUI16();
		}
		if ((getSWFVersion() >= 5) && hasClipActions)
		{
			clipActions = new ClipActions(inStream, getSWFVersion());
		}
	}

	@Override
	public String toString()
	{
		return super.toString() + ": depth=" + depth + ((characterId != 0 ? (" characterId=" + characterId) : "") + (hasName ? (" name=" + name) : ""));
	}
}
